using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Reflection;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;

namespace Caramel.ServiceModel
{
	public abstract class ExceptionHandlingProxyBase<T> : ICommunicationObject, IDisposable where T : class
	{
		// state
		private bool IsOpened { get; set; }
		public bool IsDisposed { get; private set; }

		// lock
		private object _channelLock = new object();
		private bool _isInitialized = false;
		private bool _isProxyCreated = false;
		private ManualResetEvent _proxyRecreationLock = new ManualResetEvent(true);
		protected int _proxyRecreationLockWait = 1000;

		// channel
		private ChannelFactory<T> _channelFactory = null;
		private T _channel = default(T);

		#region Constructors

		public ExceptionHandlingProxyBase()
		{
		}

		public ExceptionHandlingProxyBase(string endpointConfigurationName)
		{
			this.Initialize(endpointConfigurationName);
		}

		protected virtual void Initialize(string endpointConfigurationName)
		{
			if (_isInitialized)
			{
				throw new InvalidOperationException("Object already initialized.");
			}

			_isInitialized = true;
			_channelFactory = new ChannelFactory<T>(endpointConfigurationName);
		}

		public ExceptionHandlingProxyBase(string endpointConfigurationName, string remoteAddress)
		{
			this.Initialize(endpointConfigurationName, remoteAddress);
		}

		protected virtual void Initialize(string endpointConfigurationName, string remoteAddress)
		{
			if (_isInitialized)
			{
				throw new InvalidOperationException("Object already initialized.");
			}

			_isInitialized = true;
			_channelFactory = new ChannelFactory<T>(endpointConfigurationName, new EndpointAddress(remoteAddress));
		}

		public ExceptionHandlingProxyBase(Binding binding, EndpointAddress remoteAddress)
		{
			this.Initialize(binding, remoteAddress);
		}

		protected virtual void Initialize(Binding binding, EndpointAddress remoteAddress)
		{
			if (_isInitialized)
			{
				throw new InvalidOperationException("Object already initialized.");
			}

			_isInitialized = true;
			_channelFactory = new ChannelFactory<T>(binding, remoteAddress);
		}

		#endregion

		#region Proxy creation

		public event EventHandler AfterRecreateProxy;

		protected virtual void CreateProxy()
		{
			lock (_channelLock)
			{
				if (_isProxyCreated)
				{
					throw new InvalidOperationException("Proxy already created.");
				}

				this.CreateInnerChannel();
				_isProxyCreated = true;
			}
		}

		protected virtual void RecreateProxy()
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				this.CreateInnerChannel();

				if (this.AfterRecreateProxy != null)
				{
					this.AfterRecreateProxy(this, null);
				}
			}
		}

		private void CreateInnerChannel()
		{
			lock (_channelLock)
			{
				if (_channelFactory == null)
				{
					throw new InvalidOperationException("Proxy invalid. This occurs when you use the default constructor.");
				}

				_channel = _channelFactory.CreateChannel();

				ICommunicationObject co = _channel as ICommunicationObject;
				co.Faulted += InnerChannel_Faulted;
				co.Closed += InnerChannel_Closed;
				co.Closing += InnerChannel_Closing;
				co.Opened += InnerChannel_Opened;
				co.Opening += InnerChannel_Opening;
			}
		}
		#endregion

		#region Communication events

		private void InnerChannel_Opening(object sender, EventArgs e)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				if (this.Opening != null)
				{
					this.Opening(sender, e);
				}
			}
		}

		private void InnerChannel_Opened(object sender, EventArgs e)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				if (this.Opened != null)
				{
					this.Opened(sender, e);
				}
			}
		}

		private void InnerChannel_Closing(object sender, EventArgs e)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				if (this.Closing != null)
				{
					this.Closing(sender, e);
				}
			}
		}
		private void InnerChannel_Closed(object sender, EventArgs e)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				try
				{
					_proxyRecreationLock.Reset(); // will stop other threads from trying to Invoke() while recreating the proxy

					if (this.Closed != null)
					{
						this.Closed(sender, e);
					}

					this.OnClosed();
				}
				finally
				{
					_proxyRecreationLock.Set(); // will stop other threads from trying to Invoke() while recreating the proxy
				}
			}
		}

		protected virtual void OnClosed()
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				this.Abort();
				this.RecreateProxy();
			}
		}

		private void InnerChannel_Faulted(object sender, EventArgs e)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				try
				{
					_proxyRecreationLock.Reset(); // will stop other threads from trying to Invoke() while recreating the proxy

					if (this.Faulted != null)
					{
						this.Faulted(sender, e);
					}

					this.OnFaulted();
				}
				finally
				{
					_proxyRecreationLock.Set(); // will stop other threads from trying to Invoke() while recreating the proxy
				}
			}
		}

		protected virtual void OnFaulted()
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				this.Abort();
				RecreateProxy();
			}
		}

		#endregion

		#region Channel Properties

		public IClientChannel InnerChannel
		{
			get
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				return (IClientChannel)_channel;
			}
		}

		public ClientCredentials ClientCredentials
		{
			get
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				return _channelFactory.Credentials;
			}
		}

		public ServiceEndpoint Endpoint
		{
			get
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				return _channelFactory.Endpoint;
			}
		}

		public CommunicationState State
		{
			get
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				IChannel channel = (IChannel)_channel;

				if (channel == null)
				{
					return CommunicationState.Created;
				}

				return channel.State;
			}
		}

		#endregion

		#region ICommunicationObject Members

		public event EventHandler Closed;
		public event EventHandler Closing;
		public event EventHandler Faulted;
		public event EventHandler Opened;
		public event EventHandler Opening;

		public void Abort()
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				ICommunicationObject co = (ICommunicationObject)_channel;
				co.Closed -= new EventHandler(this.InnerChannel_Closed);
				co.Closing -= new EventHandler(this.InnerChannel_Closing);
				co.Faulted -= new EventHandler(this.InnerChannel_Faulted);
				co.Opened -= new EventHandler(this.InnerChannel_Opened);
				co.Opening -= new EventHandler(this.InnerChannel_Opening);
				co.Abort();
			}
		}

		public void Open(TimeSpan timeout)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				if (!this.IsOpened)
				{
					this.EnsureProxy();
					((ICommunicationObject)_channel).Open(timeout);
					this.IsOpened = true;
				}
			}
		}

		public void Open()
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				if (!this.IsOpened)
				{
					this.EnsureProxy();
					((ICommunicationObject)_channel).Open();
					this.IsOpened = true;
				}
			}
		}

		public void Close(TimeSpan timeout)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				((ICommunicationObject)_channel).Close(timeout);
			}
		}

		public void Close()
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				((ICommunicationObject)_channel).Close();
			}
		}

		public IAsyncResult BeginClose(TimeSpan timeout, AsyncCallback callback, object state)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				return ((ICommunicationObject)_channel).BeginClose(timeout, callback, state);
			}
		}

		public IAsyncResult BeginClose(AsyncCallback callback, object state)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				return ((ICommunicationObject)_channel).BeginClose(callback, state);
			}
		}

		public IAsyncResult BeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed)
				{
					throw new ObjectDisposedException(this.GetType().FullName);
				}

				return ((ICommunicationObject)_channel).BeginClose(timeout, callback, state);
			}
		}

		public IAsyncResult BeginOpen(AsyncCallback callback, object state)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed) throw new ObjectDisposedException(this.GetType().FullName);
				return ((ICommunicationObject)_channel).BeginOpen(callback, state);
			}
		}

		public void EndClose(IAsyncResult result)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed) throw new ObjectDisposedException(this.GetType().FullName);
				((ICommunicationObject)_channel).EndClose(result);
			}
		}

		public void EndOpen(IAsyncResult result)
		{
			lock (_channelLock)
			{
				if (this.IsDisposed) throw new ObjectDisposedException(this.GetType().FullName);
				((ICommunicationObject)_channel).EndOpen(result);
			}
		}


		#endregion

		#region IDisposable Members

		public void Dispose()
		{
			lock (_channelLock)
			{
				Cleanup();
				this.IsDisposed = true;
			}
		}

		protected virtual void Cleanup()
		{
			try
			{
				ICommunicationObject co = (ICommunicationObject)_channel;
				co.Closed -= InnerChannel_Closed;
				co.Closing -= InnerChannel_Closing;
				co.Faulted -= InnerChannel_Faulted;
				co.Opened -= InnerChannel_Opened;
				co.Opening -= InnerChannel_Opening;
				co.Close();
			}
			catch
			{
				try
				{
					ICommunicationObject co = (ICommunicationObject)_channel;
					co.Abort();
				}
				catch { }
			}

			try
			{
				_channelFactory.Close();
			}
			catch
			{
				try
				{
					_channelFactory.Abort();
				}
				catch { }
			}

		}

		#endregion

		#region Invoke

		public delegate void RetryInvokeHandler(out Message unreadMessage);
		public event RetryInvokeHandler RetryInvoke;

		//protected void Invoke(string operationName, params object[] parameters)
		//{
		//    if (this.IsDisposed)
		//    {
		//        throw new ObjectDisposedException(this.GetType().FullName);
		//    }

		//    this.Open();
		//    var methodInfo = this.GetMethod(operationName);

		//    try
		//    {
		//        // manual reset event here, turn it on when faulted
		//        // other threads will queue, and get a successful Invoke() once proxy is recreated
		//        _proxyRecreationLock.WaitOne(_proxyRecreationLockWait); // if this takes longer than 1 second we have bigger problems
		//        methodInfo.Invoke(_channel, parameters);
		//    }
		//    catch (TargetInvocationException targetEx) // Invoke() always throws this type
		//    {
		//        var commEx = targetEx.InnerException as CommunicationException;

		//        if (commEx == null)
		//        {
		//            throw targetEx.InnerException; // not a communication exception, throw it
		//        }

		//        var faultEx = commEx as FaultException;

		//        if (faultEx != null)
		//        {
		//            throw targetEx.InnerException; // the service threw a fault, throw it
		//        }

		//        try
		//        {
		//            // manual reset event here, turn it on when faulted
		//            // other threads will queue, and get a successful Invoke() once proxy is recreated
		//            _proxyRecreationLock.WaitOne(_proxyRecreationLockWait); // if this takes longer than 1 second we have bigger problems

		//            // if it is a Message type it won't work, must fire RetryInvoke() and hopefully derived class will supply the original
		//            // message to send again...
		//            if (parameters.Length == 1 && parameters[0] is Message)
		//            {
		//                Message unreadMessage;
		//                this.RetryInvoke(out unreadMessage);
		//                methodInfo.Invoke(_channel, new object[] { unreadMessage }); // a communication exception, retry once
		//            }
		//            else
		//            {
		//                methodInfo.Invoke(_channel, parameters); // a communication exception, retry once
		//            }
		//        }
		//        catch (TargetInvocationException targetEx2)
		//        {
		//            throw targetEx2.InnerException; // still failed, throw it
		//        }
		//    }
		//}

		//protected TResult Invoke<TResult>(string operationName, params object[] parameters)
		//{
		//    var methodInfo = this.GetMethod(operationName);

		//    return Invoke<TResult>(methodInfo, parameters);
		//}

		//protected TResult Invoke<TResult>(MethodInfo methodInfo, params object[] parameters)
		//{
		//    if (this.IsDisposed)
		//    {
		//        throw new ObjectDisposedException(this.GetType().FullName);
		//    }

		//    this.Open();
		//    var result = default(TResult);

		//    try
		//    {
		//        // manual reset event here, turn it on when faulted
		//        // other threads will queue, and get a successful Invoke() once proxy is recreated
		//        _proxyRecreationLock.WaitOne(_proxyRecreationLockWait); // if this takes longer than 1 second we have bigger problems
		//        result = (TResult)methodInfo.Invoke(_channel, parameters);
		//    }
		//    catch (TargetInvocationException targetEx) // Invoke() always throws this type
		//    {
		//        var commEx = targetEx.InnerException as CommunicationException;

		//        if (commEx == null)
		//        {
		//            throw targetEx.InnerException; // not a communication exception, throw it
		//        }

		//        var faultEx = commEx as FaultException;

		//        if (faultEx != null)
		//        {
		//            throw targetEx.InnerException; // the service threw a fault, throw it
		//        }

		//        // a communication exception, retry once
		//        try
		//        {
		//            // manual reset event here, turn it on when faulted
		//            // other threads will queue, and get a successful Invoke() once proxy is recreated
		//            _proxyRecreationLock.WaitOne(_proxyRecreationLockWait); // if this takes longer than 1 second we have bigger problems

		//            // if it is a Message type it won't work, must fire RetryInvoke() and hopefully derived class will supply the original
		//            // message to send again...
		//            if (parameters.Length == 1 && parameters[0] is Message)
		//            {
		//                Message unreadMessage;
		//                RetryInvoke(out unreadMessage);
		//                result = (TResult)methodInfo.Invoke(_channel, new object[] { unreadMessage }); // communication exception, retry once
		//            }
		//            else
		//            {
		//                result = (TResult)methodInfo.Invoke(_channel, parameters); // communication exception, retry once
		//            }
		//        }
		//        catch (TargetInvocationException targetEx2)
		//        {
		//            throw targetEx2.InnerException; // still failed, throw it
		//        }
		//    }

		//    return result;
		//}

		//internal MethodInfo GetMethod(string operationName)
		//{
		//    var t = typeof(T);

		//    var methods = new HashSet<MethodInfo>();
		//    this.GetMethodsRecursive(t, BindingFlags.Public | BindingFlags.Instance, ref methods);

		//    var result =
		//        methods.
		//            Where(m => m.Name == operationName);

		//    if (result.Count() == 0)
		//    {
		//        throw new InvalidOperationException(String.Format("Unable to invoke method {0}. Method does not exist on contract {1}.", operationName, t.ToString()));
		//    }

		//    if (result.Count() > 1)
		//    {
		//        throw new InvalidOperationException(String.Format("Unable to invoke method {0}. More than one method is defined on contract {1} by the same name. Overloads not supported by CachedProxyBase.", operationName, t.ToString()));
		//    }

		//    return result.First();
		//}

		//private void GetMethodsRecursive(Type t, BindingFlags flags, ref HashSet<MethodInfo> methods)
		//{
		//    var children = t.GetMethods(flags);
		//    methods.UnionWith(children);

		//    foreach (var contract in t.GetInterfaces())
		//    {
		//        this.GetMethodsRecursive(contract, flags, ref methods);
		//    }
		//}

		private void EnsureProxy()
		{
			lock (_channelLock)
			{
				if (!_isProxyCreated)
				{
					this.CreateProxy();
				}
			}
		}

		#endregion

		public void Invoke(Expression<Action<T>> expression)
		{
			if (this.IsDisposed)
			{
				throw new ObjectDisposedException(this.GetType().FullName);
			}

			this.Open();

			var body = expression.Body;
			var call = body as MethodCallExpression;
			var parameters = new List<object>();

			foreach (var argument in call.Arguments)
			{
				var constant = argument as ConstantExpression;

				if (constant != null)
				{
					parameters.Add(constant.Value);
				}
				else
				{
					Expression<Func<object>> lambda = Expression.Lambda<Func<object>>(Expression.Convert(argument, typeof(object)));
					Func<object> func = lambda.Compile();
					parameters.Add(func());
				}
			}

			try
			{
				// manual reset event here, turn it on when faulted
				// other threads will queue, and get a successful Invoke() once proxy is recreated
				_proxyRecreationLock.WaitOne(_proxyRecreationLockWait); // if this takes longer than 1 second we have bigger problems
				call.Method.Invoke(_channel, parameters.ToArray());
			}
			catch (TargetInvocationException targetEx) // Invoke() always throws this type
			{
				var commEx = targetEx.InnerException as CommunicationException;

				if (commEx == null)
				{
					throw targetEx.InnerException; // not a communication exception, throw it
				}

				var faultEx = commEx as FaultException;

				if (faultEx != null)
				{
					throw targetEx.InnerException; // the service threw a fault, throw it
				}

				try
				{
					// manual reset event here, turn it on when faulted
					// other threads will queue, and get a successful Invoke() once proxy is recreated
					_proxyRecreationLock.WaitOne(_proxyRecreationLockWait); // if this takes longer than 1 second we have bigger problems

					// if it is a Message type it won't work, must fire RetryInvoke() and hopefully derived class will supply the original
					// message to send again...
					if (parameters.Count() == 1 && parameters[0] is Message)
					{
						Message unreadMessage;
						this.RetryInvoke(out unreadMessage);
						call.Method.Invoke(_channel, new object[] { unreadMessage }); // a communication exception, retry once
					}
					else
					{
						call.Method.Invoke(_channel, parameters.ToArray()); // a communication exception, retry once
					}
				}
				catch (TargetInvocationException targetEx2)
				{
					throw targetEx2.InnerException; // still failed, throw it
				}
			}
		}

		public TResult Invoke<TResult>(Expression<Action<T>> expression)
		{
			if (this.IsDisposed)
			{
				throw new ObjectDisposedException(this.GetType().FullName);
			}

			this.Open();

			var body = expression.Body;
			var call = body as MethodCallExpression;
			var parameters = new List<object>();

			foreach (var argument in call.Arguments)
			{
				var constant = argument as ConstantExpression;

				if (constant != null)
				{
					parameters.Add(constant.Value);
				}
				else
				{
					Expression<Func<object>> lambda = Expression.Lambda<Func<object>>(Expression.Convert(argument, typeof(object)));
					Func<object> func = lambda.Compile();
					parameters.Add(func());
				}
			}

			try
			{
				// manual reset event here, turn it on when faulted
				// other threads will queue, and get a successful Invoke() once proxy is recreated
				_proxyRecreationLock.WaitOne(_proxyRecreationLockWait); // if this takes longer than 1 second we have bigger problems

				return (TResult)call.Method.Invoke(_channel, parameters.ToArray());
			}
			catch (TargetInvocationException targetEx) // Invoke() always throws this type
			{
				var commEx = targetEx.InnerException as CommunicationException;

				if (commEx == null)
				{
					throw targetEx.InnerException; // not a communication exception, throw it
				}

				var faultEx = commEx as FaultException;

				if (faultEx != null)
				{
					throw targetEx.InnerException; // the service threw a fault, throw it
				}

				try
				{
					// manual reset event here, turn it on when faulted
					// other threads will queue, and get a successful Invoke() once proxy is recreated
					_proxyRecreationLock.WaitOne(_proxyRecreationLockWait); // if this takes longer than 1 second we have bigger problems

					// if it is a Message type it won't work, must fire RetryInvoke() and hopefully derived class will supply the original
					// message to send again...
					if (parameters.Count() == 1 && parameters[0] is Message)
					{
						Message unreadMessage;
						this.RetryInvoke(out unreadMessage);

						return (TResult)call.Method.Invoke(_channel, new object[] { unreadMessage }); // a communication exception, retry once
					}
					else
					{
						return (TResult)call.Method.Invoke(_channel, parameters.ToArray()); // a communication exception, retry once
					}
				}
				catch (TargetInvocationException targetEx2)
				{
					throw targetEx2.InnerException; // still failed, throw it
				}
			}
		}
	}
}
